# Music importer.

import logging
import os
import os.path
import sha
import subprocess
import threading
import time
from mutagen import oggvorbis, easyid3, mp3
from django.conf import settings
from django.db import transaction
from streamux.radio import models

IDLE_INTERVAL = 10 # seconds

log = logging.getLogger('importer')

class ProcessingError(RuntimeError):
    """An error occured trying to process an incoming file."""

DEFAULT_TAGS = {
    'album': None,
    'mbid': '',
    'artist_mbid': '',
    'album_mbid': ''
    }

# All supported extensions, and their associated tag decoders and
# exception base classes.
_SUPPORTED_EXTENSIONS = ('ogg', 'mp3')

def get_tags_mp3(filename):
    try:
        tag = mp3.MP3(filename)
    except (mp3.error, easyid3.error):
        raise ProcessingError('Could not parse ID3 tag')

    if 'TIT2' not in tag:
        raise ProcessingError('No title in ID3 tag')
    if 'TPE1' not in tag:
        raise ProcessingError('No artist in ID3 tag')

    out = DEFAULT_TAGS.copy()
    out['title'] = make_unicode(tag['TIT2'].text[0])
    out['artist'] = make_unicode(tag['TPE1'].text[0])

    if 'TALB' in tag:
        out['album'] = make_unicode(tag['TALB'].text[0])
    if 'UFID:http://musicbrainz.org' in tag:
        out['mbid'] = tag['UFID:http://musicbrainz.org'].data
    if 'TXXX:MusicBrainz Album Id' in tag:
        out['album_mbid'] = tag['TXXX:MusicBrainz Album Id'].text[0]
    if 'TXXX:MusicBrainz Artist Id' in tag:
        out['artist_mbid'] = tag['TXXX:MusicBrainz Artist Id'].text[0]

    return out

def get_tags_ogg(filename):
    try:
        tag = oggvorbis.OggVorbis(filename)
    except oggvorbis.error:
        raise ProcessingError('Could not parse Ogg Vorbis tag')

    if 'title' not in tag:
        raise ProcessingError('No title in Ogg tag')
    if 'artist' not in tag:
        raise ProcessingError('No artist in Ogg tag')

    out = DEFAULT_TAGS.copy()
    out['title'] = make_unicode(tag['title'][0])
    out['artist'] = make_unicode(tag['artist'][0])

    if 'album' in tag:
        out['album'] = make_unicode(tag['album'][0])
    if 'musicbrainz_trackid' in tag:
        out['mbid'] = tag['musicbrainz_trackid'][0]
    if 'musicbrainz_albumid' in tag:
        out['album_mbid'] = tag['musicbrainz_albumid'][0]
    if 'musicbrainz_artistid' in tag:
        out['artist_mbid'] = tag['musicbrainz_artistid'][0]

    return out

_TAG_DECODERS = {
    'ogg': get_tags_ogg,
    'mp3': get_tags_mp3,
    }

def replay_gain_ogg(filepath):
    output = subprocess.Popen('nice vorbisgain -n -d "%s"' % filepath,
                              shell=True,
                              stdout=subprocess.PIPE).stdout.read()

    if not output:
        raise ProcessingError('vorbisgain failed to run')

    log.debug('vorbisgain output: ' + output)

    # Given vorbisgain's output, we want the first 'word' of the last
    # line.
    return output.strip().split('\n')[-1].split()[0]

def replay_gain_mp3(filepath):
    log.debug('Running mp3gain on ' + filepath)
    output = subprocess.Popen('nice mp3gain -s s -q -o "%s"' % filepath,
                              shell=True,
                              stdout=subprocess.PIPE).stdout.read()

    if not output:
        raise ProcessingError('mp3gain failed to run')

    log.debug('mp3gain output: ' + output)

    # mp3gain's "database" output places the dB replay gain in the 3rd
    # field of the last line. Actually, the last line is "album" gain,
    # which is identical to track gain for a single track.
    return output.strip().split('\n')[-1].split('\t')[2]

# All supported extensions, and their associated replay gain calculation functions.
_GAIN_CALCULATORS = {
    'ogg': replay_gain_ogg,
    'mp3': replay_gain_mp3
}

def calculate_replaygain(filepath, extension):
    try:
        gain = _GAIN_CALCULATORS[extension](filepath)
    except IndexError:
        raise ProcessingError('Gain calculator did not return gain')

    # Check that the gain does look like a floating point number
    try:
        float(gain)
    except ValueError:
        raise ProcessingError(
            'Invalid gain returned: ' + gain)

    return gain

def get_extension(filepath):
    if '.' not in filepath:
        raise ProcessingError('No extension found')

    extension = filepath.rsplit('.', 1)[1]
    if extension not in _SUPPORTED_EXTENSIONS:
        raise ProcessingError('Unsupported extension .' + extension)
    return extension

def make_unicode(input):
    if isinstance(input, unicode):
        return input
    try:
        return input.decode('utf-8')
    except UnicodeDecodeError:
        raise ProcessingError('Unhandled tag encoding')

def content_hash(filepath):
    fd = open(filepath)
    hash = sha.new(fd.read()).hexdigest()
    fd.close()
    return hash

FILECHARS_TRANSLATE_TABLE = [chr(x) for x in xrange(256)]
FILECHARS_TRANSLATE_TABLE[:45] = 45*'_'
FILECHARS_TRANSLATE_TABLE[47] = '_'
FILECHARS_TRANSLATE_TABLE[58:65] = 7*'_'
FILECHARS_TRANSLATE_TABLE[91:97] = 6*'_'
FILECHARS_TRANSLATE_TABLE[123:] = 133*'_'
FILECHARS_TRANSLATE_TABLE = ''.join(FILECHARS_TRANSLATE_TABLE)

def install_file(filepath, extension, tags):
    # The new name of the file contains sanitized versions of all
    # its components, as well as a content hash.
    hash = content_hash(filepath)

    artist = tags['artist'].encode(
        'ascii', 'replace').translate(FILECHARS_TRANSLATE_TABLE)
    if tags['album']:
        album = tags['album'].encode(
            'ascii', 'replace').translate(FILECHARS_TRANSLATE_TABLE)
    else:
        album = 'NoAlbum'
    track = tags['title'].encode(
        'ascii', 'replace').translate(FILECHARS_TRANSLATE_TABLE)

    new_filename = u'%s_%s_%s_%s.%s' % (hash, artist, album,
                                        track, extension)
    new_filedir = os.path.join(settings.MUSIC_DIR, artist)
    new_filepath = os.path.join(new_filedir, new_filename)

    logging.debug('Installing %s as %s' % (filepath, new_filepath))
    if not os.path.exists(new_filedir):
        os.makedirs(new_filedir)
    os.rename(filepath, new_filepath)

    return new_filepath

def register_db_file(filepath, tags, replay_gain):
    # First check if we know any of the artist, album or track, by
    # name or MBID (if provided)
    artist = models.Artist.objects.get_or_create(
        name=tags['artist'], mbid=tags['artist_mbid'])[0]
    artist.save()

    if tags['album']:
        album = models.Album.objects.get_or_create(
            name=tags['album'], mbid=tags['album_mbid'],
            artist=artist)[0]
        album.save()
    else:
        album = None

    assert filepath.startswith(settings.MUSIC_DIR)
    filepath = filepath[len(settings.MUSIC_DIR):]

    track = models.Track.objects.get_or_create(
        name=tags['title'], artist=artist, album=album, mbid=tags['mbid'],
        replay_gain=replay_gain, file=filepath)[0]
    track.save()

def fail(filepath):
    hash = content_hash(filepath)

    new_filename = '%s_%s' % (hash, os.path.basename(filepath))
    new_filepath = os.path.join(settings.MUSIC_FAILED, new_filename)

    log.error('Saved failed track as ' + new_filename)
    os.rename(filepath, new_filepath)
    log.debug('Renamed %s to %s' % (filepath, new_filepath))

def process_file(filepath):
    log.info('Processing new file ' + filepath)
    extension = get_extension(filepath)
    tags = _TAG_DECODERS[extension](filepath)

    replay_gain = calculate_replaygain(filepath, extension)
    filepath = install_file(filepath, extension, tags)
    register_db_file(filepath, tags, replay_gain)
    log.info('Successfully registered new track: %s - %s'
             % (tags['artist'], tags['title']))

def process_incoming():
    processed = 0
    for root, _, files in os.walk(settings.MUSIC_INCOMING):
        for file in files:
            filepath = os.path.join(root, file)
            try:
                process_file(filepath)
            except ProcessingError, e:
                log.error('Processing error: %s' % e.args[0])
                fail(filepath)
            processed += 1
    log.debug('Run complete, processed %d new files' % processed)


class MusicImporter(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.setDaemon(True)

    def run(self):
        log.info('Importer thread started')
        log.info('Monitoring ' + settings.MUSIC_INCOMING)

        while True:
            if not process_incoming():
                time.sleep(IDLE_INTERVAL)



